<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <meta http-equiv="Content-Style-Type" content="text/css" />
  <meta name="generator" content="pandoc" />
  <title>内存管理</title>
  <style type="text/css">code{white-space: pre;}</style>
  <link rel="stylesheet" href="doc/linux-style.css" type="text/css" />
</head>
<body>
<div id="header">
<h1 class="title">内存管理</h1>
</div>
<div id="TOC">
<ul>
<li><a href="#内存管理">内存管理</a><ul>
<li><a href="#三种地址">三种地址</a></li>
<li><a href="#内存管理的方法">内存管理的方法</a></li>
<li><a href="#物理页面描述符-struct-page">1 物理页面描述符 <code>struct page</code></a></li>
<li><a href="#内存分配与释放">2 内存分配与释放</a></li>
<li><a href="#页面缓存与-struct-folio">3 页面缓存与 <code>struct folio</code></a></li>
<li><a href="#区zone">4 区（Zone）</a></li>
<li><a href="#伙伴算法">5 伙伴算法</a></li>
<li><a href="#虚拟内存管理">6 虚拟内存管理</a></li>
<li><a href="#slab-分配器">7 Slab 分配器</a></li>
<li><a href="#总结">总结</a></li>
</ul></li>
</ul>
</div>
<h1 id="内存管理">内存管理</h1>
<p>学过操作系统的我们都知道，操作系统的诞生就是为了让用户不能直接的去操控硬件，而只需要提供一套调用接口给用户使用。那么为了不让用户直接操作内存的地址，这个是时候就诞生出来内存管理。</p>
<h2 id="三种地址">三种地址</h2>
<p>首先分清楚三种地址的区别： - 逻辑地址：是程序代码中可以直接只用的地址，由虚拟地址经过段机制转化而来，由段选择子+段内偏移量组成。 - 虚拟地址：连续的平坦地址空间，由物理地址经过页机制转换而来。 - 物理地址：直接的硬件地址。</p>
<p>这里大概讲一下段机制和页机制</p>
<p>段机制，程序代码需要分成.bss .data .txt 等各种段，也就是把逻辑和数据分开存储，那么就可以将这些内容放在 一个特定的段，然后经过地址转换---虚拟地址= 段基地址 + 段内偏移量。</p>
<p>页机制，其实最主要的是建立一个映射，当一个进程需要调用内存的时候，先将内存分成一块一块的，称为页帧，然后再将虚拟地址分成一块一块的，通常为4KB，称为页,然后建立一个页表，通过页表去查询是哪一块内存。通常现在的也表都是多级页表，也就是嵌套了多个也表，类似于这一页实际上在上一级页表的某一块。</p>
<h2 id="内存管理的方法">内存管理的方法</h2>
<p>内存管理主要包括虚地址、地址变换、内存分配和回收、内存扩充、内存共享和保护等功能。</p>
<p>内存的管理单元由MMU完成，用户程序看到的地址（如0x4000），由操作系统+硬件（MMU）动态映射到物理地址。</p>
<p><strong>mmu是怎么转换的呢？</strong> 通过 页表（Page Table） 动态转换地址。</p>
<p>不过在linux下的段机制被弱化了，主要通过页机制进行分配</p>
<h2 id="物理页面描述符-struct-page">1 物理页面描述符 <code>struct page</code></h2>
<p>系统中的每个物理页面（通常为 4KB 大小，可通过 <code>getconf -a | grep PAGESIZE</code> 查看）都有一个 <code>struct page</code> 来描述它。这个结构体相当于每个物理页面的 “身份证”，记录了页面的各种信息：</p>
<ul>
<li><strong><code>flags</code></strong>：用二进制位记录页面的状态，例如是否出错、是否私有等。内核中定义了很多类似 <code>SetPageError</code>、<code>PagePrivate</code> 的宏来设置或检测这些状态位，它们的本质都是通过位操作来修改或读取 <code>flags</code> 字段。</li>
<li><strong><code>refcount</code></strong>：记录页面被引用的次数。当引用次数为 0 时，页面处于空闲状态。</li>
<li><strong><code>virtual</code></strong>：在一些特殊情况下（比如 highmem 内存区域），用于存储页面的内核虚拟地址。</li>
<li><strong><code>union page_union_1 和 union page_union_2</code></strong>：这两个联合体包含页面的多种用途相关信息，比如页面在页面缓存中的位置、网络栈使用的信息等。</li>
</ul>
<h2 id="内存分配与释放">2 内存分配与释放</h2>
<h3 id="分配函数">2.1 分配函数</h3>
<p>内核提供了多种函数来分配内存，常见的有：</p>
<ul>
<li><strong><code>alloc_pages(gfp_t gfp_mask, unsigned int order)</code></strong>：基础分配函数，用于分配 <code>2^order</code> 个连续的物理页面。例如，<code>order</code> 为 0 时分配 1 个页面，<code>order</code> 为 1 时分配 2 个页面等。</li>
<li><strong><code>__get_free_pages(gfp_t gfp_mask, unsigned int order)</code></strong>：与 <code>alloc_pages</code> 类似，但它返回逻辑地址而不是 <code>struct page</code> 指针。</li>
<li><strong><code>kmalloc(size_t size, gfp_t gfp)</code></strong>：分配物理地址连续、大小为 <code>size</code> 的内存块，通常用于分配较小的内存。</li>
<li><strong><code>vmalloc(unsigned long size)</code></strong>：分配虚拟地址连续（但物理地址可能不连续）的大块内存，适用于模块加载等场景。</li>
</ul>
<h3 id="释放函数">2.2 释放函数</h3>
<p>对应的释放函数包括：</p>
<ul>
<li><strong><code>__free_pages(struct page *page, unsigned int order)</code></strong>：释放通过 <code>alloc_pages</code> 分配的页面。</li>
<li><strong><code>free_pages(unsigned long addr, unsigned int order)</code></strong>：释放通过 <code>__get_free_pages</code> 分配的页面。</li>
<li><strong><code>kfree(void *p)</code></strong>：释放通过 <code>kmalloc</code> 分配的内存。</li>
<li><strong><code>vfree(const void *addr)</code></strong>：释放通过 <code>vmalloc</code> 分配的内存。</li>
</ul>
<h3 id="gfp_t-标志">2.3 <code>gfp_t</code> 标志</h3>
<p><code>gfp_t</code> 是内存分配时使用的标志类型，用于告诉内核如何分配内存。常见的标志有：</p>
<ul>
<li><strong><code>GFP_ATOMIC</code></strong>：适用于原子上下文（如中断处理程序），分配内存时不能睡眠。</li>
<li><strong><code>GFP_KERNEL</code></strong>：适用于内核线程，可以睡眠，分配内存时会尝试直接回收内存。</li>
<li><strong><code>__GFP_DMA</code></strong>：表示需要从 DMA 区域分配内存，用于外设直接内存访问。</li>
<li><strong><code>__GFP_HIGHMEM</code></strong>：用于分配高端内存（highmem），这种内存没有永久映射到内核地址空间，需通过 <code>kmap</code> 等函数临时映射。</li>
</ul>
<h2 id="页面缓存与-struct-folio">3 页面缓存与 <code>struct folio</code></h2>
<ul>
<li><strong>页面缓存</strong>：为了提高文件读写效率，内核会缓存经常访问的文件页面。这些页面通过 <code>struct page</code> 来描述。</li>
<li><strong><code>struct folio</code></strong>：这是多个连续页面的抽象，可以理解为多个页面的集合。它使内核能够更高效地管理大块连续内存，例如透明大页（Transparent Huge Pages, THP）。</li>
</ul>
<h2 id="区zone">4 区（Zone）</h2>
<p>物理内存被逻辑上划分为不同的区域（zone），常见的区域包括：</p>
<ul>
<li><strong><code>ZONE_DMA</code></strong>：用于外设 DMA 操作的低地址内存区域。</li>
<li><strong><code>ZONE_NORMAL</code></strong>：常规内存区域，内核可以直接访问。</li>
<li><strong><code>ZONE_HIGHMEM</code></strong>：高端内存区域，内核不能直接访问，需要临时映射。</li>
<li><strong><code>ZONE_MOVABLE</code></strong>：可移动内存区域，主要用于内存下线等场景。</li>
</ul>
<p>每个区域由 <code>struct zone</code> 描述，其中记录了区域的水印（watermark）、空闲页面链表等信息。</p>
<h2 id="伙伴算法">5 伙伴算法</h2>
<p>伙伴算法是内核管理物理页面分配和释放的核心算法。它的基本思想是将物理页面组织成大小为 <code>2^order</code> 的块（称为页块或伙伴块）。当需要分配内存时，内核会查找合适大小的空闲块；当释放内存时，会尝试将相邻的空闲块合并成更大的块。</p>
<ul>
<li><strong><code>struct free_area</code></strong>：每个 <code>struct zone</code> 中都有一个 <code>free_area[MAX_ORDER + 1]</code> 数组，其中 <code>free_area[order]</code> 存储大小为 <code>2^order</code> 的空闲块链表。</li>
<li><strong>分配过程</strong>：例如，要分配 <code>2^3</code> 个页面，内核会先查找 <code>free_area[3]</code> 中的空闲块。如果找不到，就会从更大的块（如 <code>free_area[4]</code>）中拆分出合适大小的块。</li>
<li><strong>释放过程</strong>：释放一个页面块时，内核会检查它是否有伙伴块（大小相同且相邻的块）。如果有，就将它们合并成更大的块，并插入到对应大小的空闲链表中。</li>
</ul>
<h2 id="虚拟内存管理">6 虚拟内存管理</h2>
<h3 id="进程地址空间">6.1 进程地址空间</h3>
<p>每个进程都有独立的虚拟地址空间，内核用 <code>struct mm_struct</code> 描述这个地址空间。进程的虚拟地址空间被划分为多个虚拟内存区域（VMA），每个 VMA 由 <code>struct vm_area_struct</code> 描述。</p>
<ul>
<li><strong><code>struct mm_struct</code></strong>：记录进程的页表、内存使用统计等信息。</li>
<li><strong><code>struct vm_area_struct</code></strong>：描述一个连续的虚拟内存区间，如代码段、数据段、堆、栈等。每个 VMA 都有起始地址、结束地址、访问权限等属性。</li>
</ul>
<h3 id="页表">6.2 页表</h3>
<p>虚拟内存和物理内存的映射关系通过页表实现。常见的页表结构包括：</p>
<ul>
<li><strong><code>PGD</code>（页全局目录）</strong>：顶级页表。</li>
<li><strong><code>PMD</code>（页中间目录）</strong>：二级页表。</li>
<li><strong><code>PTE</code>（页表项）</strong>：三级页表，包含物理页面的地址。</li>
</ul>
<p>内核通过维护页表，将进程的虚拟地址映射到物理地址。为了加速映射过程，处理器还使用了 Translation Lookaside Buffer（TLB）缓存。</p>
<h3 id="内存映射操作">6.3 内存映射操作</h3>
<ul>
<li><strong><code>mmap</code></strong>：将文件或设备内存映射到进程的地址空间，返回一个虚拟内存区域。</li>
<li><strong><code>munmap</code></strong>：取消内存映射，释放对应的虚拟内存区域。</li>
</ul>
<h2 id="slab-分配器">7 Slab 分配器</h2>
<p>Slab 分配器用于管理内核中频繁分配和释放的小对象（如 inode、dentry 等）。它通过缓存预分配的内存块来提升分配效率。</p>
<ul>
<li><strong><code>struct kmem_cache</code></strong>：表示一个缓存，包含多个 slab。</li>
<li><strong><code>struct slab</code></strong>：表示一个 slab，由一个或多个物理连续的页面组成，包含多个对象。</li>
</ul>
<p>Slab 分配器的主要接口包括：</p>
<ul>
<li><strong><code>kmem_cache_create</code></strong>：创建一个缓存。</li>
<li><strong><code>kmem_cache_alloc</code></strong>：从缓存中分配一个对象。</li>
<li><strong><code>kmem_cache_free</code></strong>：释放一个对象到缓存中。</li>
</ul>
<h2 id="总结">总结</h2>
<p>Linux 内核的内存管理涉及多个层面，包括物理页面管理、内存分配与释放、虚拟内存管理等。<code>struct page</code> 是物理页面的核心描述符，伙伴算法负责物理页面的分配和回收，<code>struct mm_struct</code> 和 <code>struct vm_area_struct</code> 描述进程的虚拟地址空间，页表实现虚拟地址到物理地址的映射，Slab 分配器管理内核小对象的分配。这些组件协同工作，确保系统内存的高效利用和稳定运行。</p>
</body>
</html>
